<?php
/**
 * Provides a common API for different databases - will automatically use any installed extension
 * 
 * This class is implemented to use the UTF-8 character encoding. Please see
 * http://flourishlib.com/docs/UTF-8 for more information.
 * 
 * The following databases are supported:
 * 
 *  - [http://microsoft.com/sql/ MSSQL]
 *  - [http://mysql.com MySQL]
 *  - [http://oracle.com Oracle]
 *  - [http://postgresql.org PostgreSQL]
 *  - [http://sqlite.org SQLite]
 * 
 * The class will automatically use the first of the following extensions it finds:
 * 
 *  - MSSQL (via ODBC)
 *   - [http://php.net/pdo_odbc pdo_odbc]
 *   - [http://php.net/odbc odbc]
 *  - MSSQL
 *   - [http://msdn.microsoft.com/en-us/library/cc296221.aspx sqlsrv]
 *   - [http://php.net/pdo_dblib pdo_dblib]
 *   - [http://php.net/mssql mssql] (or [http://php.net/sybase sybase])
 *  - MySQL
 *   - [http://php.net/mysql mysql]
 *   - [http://php.net/mysqli mysqli]
 *   - [http://php.net/pdo_mysql pdo_mysql]
 *  - Oracle (via ODBC)
 *   - [http://php.net/pdo_odbc pdo_odbc]
 *   - [http://php.net/odbc odbc]
 *  - Oracle
 *   - [http://php.net/oci8 oci8]
 *   - [http://php.net/pdo_oci pdo_oci]
 *  - PostgreSQL
 *   - [http://php.net/pgsql pgsql]
 *   - [http://php.net/pdo_pgsql pdo_pgsql]
 *  - SQLite
 *   - [http://php.net/pdo_sqlite pdo_sqlite] (for v3.x)
 *   - [http://php.net/sqlite sqlite] (for v2.x)
 * 
 * @copyright  Copyright (c) 2007-2009 Will Bond
 * @author     Will Bond [wb] <will@flourishlib.com>
 * @license    http://flourishlib.com/license
 * 
 * @package    Flourish
 * @link       http://flourishlib.com/fDatabase
 * 
 * @version    1.0.0b18
 * @changes    1.0.0b18  Updated the class for the new fResult and fUnbufferedResult APIs, fixed ::unescape() to not touch NULLs [wb, 2009-08-12]
 * @changes    1.0.0b17  Added the ability to pass an array of all values as a single parameter to ::escape() instead of one value per parameter [wb, 2009-08-11]
 * @changes    1.0.0b16  Fixed PostgreSQL and Oracle from trying to get auto-incrementing values on inserts when explicit values were given [wb, 2009-08-06]
 * @changes    1.0.0b15  Fixed a bug where auto-incremented values would not be detected when table names were quoted [wb, 2009-07-15]
 * @changes    1.0.0b14  Changed ::determineExtension() and ::determineCharacterSet() to be protected instead of private [wb, 2009-07-08]
 * @changes    1.0.0b13  Updated ::escape() to accept arrays of values for insertion into full SQL strings [wb, 2009-07-06]
 * @changes    1.0.0b12  Updates to ::unescape() to improve performance [wb, 2009-06-15]
 * @changes    1.0.0b11  Changed replacement values in preg_replace() calls to be properly escaped [wb, 2009-06-11]
 * @changes    1.0.0b10  Changed date/time/timestamp escaping from `strtotime()` to fDate/fTime/fTimestamp for better localization support [wb, 2009-06-01]
 * @changes    1.0.0b9   Fixed a bug with ::escape() where floats that start with a . were encoded as `NULL` [wb, 2009-05-09]
 * @changes    1.0.0b8   Added Oracle support, change PostgreSQL code to no longer cause lastval() warnings, added support for arrays of values to ::escape() [wb, 2009-05-03]
 * @changes    1.0.0b7   Updated for new fCore API [wb, 2009-02-16]
 * @changes    1.0.0b6   Fixed a bug with executing transaction queries when using the mysqli extension [wb, 2009-02-12]
 * @changes    1.0.0b5   Changed @ error suppression operator to `error_reporting()` calls [wb, 2009-01-26]
 * @changes    1.0.0b4   Added a few error suppression operators back in so that developers don't get errors and exceptions [wb, 2009-01-14]
 * @changes    1.0.0b3   Removed some unnecessary error suppresion operators [wb, 2008-12-11]
 * @changes    1.0.0b2   Fixed a bug with PostgreSQL when using the PDO extension and executing an INSERT statement [wb, 2008-12-11]
 * @changes    1.0.0b    The initial implementation [wb, 2007-09-25]
 */
class fDatabase
{
	/**
	 * Composes text using fText if loaded
	 * 
	 * @param  string  $message    The message to compose
	 * @param  mixed   $component  A string or number to insert into the message
	 * @param  mixed   ...
	 * @return string  The composed and possible translated message
	 */
	static protected function compose($message)
	{
		$args = array_slice(func_get_args(), 1);
		
		if (class_exists('fText', FALSE)) {
			return call_user_func_array(
				array('fText', 'compose'),
				array($message, $args)
			);
		} else {
			return vsprintf($message, $args);
		}
	}
	
	
	/**
	 * An fCache object to cache the schema info to
	 * 
	 * @var fCache
	 */
	private $cache;
	
	/**
	 * Database connection resource or PDO object
	 * 
	 * @var mixed
	 */
	private $connection;
	
	/**
	 * The database name
	 * 
	 * @var string
	 */
	private $database;
	
	/**
	 * If debugging is enabled
	 * 
	 * @var boolean
	 */
	private $debug;
	
	/**
	 * The extension to use for the database specified
	 * 
	 * Options include:
	 * 
	 *  - `'mssql'`
	 *  - `'mysql'`
	 *  - `'mysqli'`
	 *  - `'oci8'`
	 *  - `'odbc'`
	 *  - `'pgsql'`
	 *  - `'sqlite'`
	 *  - `'sqlsrv'`
	 *  - `'pdo'`
	 * 
	 * @var string
	 */
	protected $extension;
	
	/**
	 * The host the database server is located on
	 * 
	 * @var string
	 */
	private $host;
	
	/**
	 * If a transaction is in progress
	 * 
	 * @var boolean
	 */
	private $inside_transaction;
	
	/**
	 * The password for the user specified
	 * 
	 * @var string
	 */
	private $password;
	
	/**
	 * The port number for the host
	 * 
	 * @var string
	 */
	private $port;
	
	/**
	 * The total number of seconds spent executing queries
	 * 
	 * @var float
	 */
	private $query_time;
	
	/**
	 * A cache of database-specific code
	 * 
	 * @var array 
	 */
	protected $schema_info;
	
	/**
	 * The millisecond threshold for triggering a warning about SQL performance
	 * 
	 * @var integer
	 */
	private $slow_query_threshold;
	
	/**
	 * The fSQLTranslation object for this database
	 * 
	 * @var object
	 */
	private $translation;
	
	/**
	 * The database type: `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, or `'sqlite'`
	 * 
	 * @var string
	 */
	private $type;
	
	/**
	 * The unbuffered query instance
	 * 
	 * @var fUnbufferedResult
	 */
	private $unbuffered_result;
	
	/**
	 * The user to connect to the database as
	 * 
	 * @var string
	 */
	private $username;
	
	
	/**
	 * Configures the connection to a database - connection is not made until the first query is executed
	 * 
	 * @param  string  $type      The type of the database: `'mssql'`, `'mysql'`, `'oracle'`, `'postgresql'`, `'sqlite'`
	 * @param  string  $database  Name of the database. If an ODBC connection `'dsn:'` concatenated with the DSN, if SQLite the path to the database file.
	 * @param  string  $username  Database username - not used for SQLite
	 * @param  string  $password  The password for the username specified - not used for SQLite
	 * @param  string  $host      Database server host or IP, defaults to localhost - not used for SQLite or ODBC connections. MySQL socket connection can be made by entering `'sock:'` followed by the socket path. PostgreSQL socket connection can be made by passing just `'sock:'`. 
	 * @param  integer $port      The port to connect to, defaults to the standard port for the database type specified - not used for SQLite or ODBC connections 
	 * @return fDatabase
	 */
	public function __construct($type, $database, $username=NULL, $password=NULL, $host=NULL, $port=NULL)
	{
		$valid_types = array('mssql', 'mysql', 'oracle', 'postgresql', 'sqlite');
		if (!in_array($type, $valid_types)) {
			throw new fProgrammerException(
				'The database type specified, %1$s, is invalid. Must be one of: %2$s.',
				$type,
				join(', ', $valid_types)
			);
		}
		
		if (empty($database)) {
			throw new fProgrammerException('No database was specified');
		}
		
		if ($host === NULL) {
			$host = 'localhost';
		}
		
		$this->type     = $type;
		$this->database = $database;
		$this->username = $username;
		$this->password = $password;
		$this->host     = $host;
		$this->port     = $port;
		
		$this->schema_info = array();
		
		$this->determineExtension();
	}
	
	
	/**
	 * Closes the open database connection
	 * 
	 * @internal
	 * 
	 * @return void
	 */
	public function __destruct()
	{
		if (!$this->connection) { return; }
		
		fCore::debug('Total query time: ' . $this->query_time . ' seconds', $this->debug);
		if ($this->extension == 'mssql') {
			mssql_close($this->connection);
		} elseif ($this->extension == 'mysql') {
			mysql_close($this->connection);
		} elseif ($this->extension == 'mysqli') {
			mysqli_close($this->connection);
		} elseif ($this->extension == 'oci8') {
			oci_close($this->connection);
		} elseif ($this->extension == 'odbc') {
			odbc_close($this->connection);
		} elseif ($this->extension == 'pgsql') {
			pg_close($this->connection);
		} elseif ($this->extension == 'sqlite') {
			sqlite_close($this->connection);
		} elseif ($this->extension == 'sqlsrv') {
			sqlsrv_close($this->connection);
		} elseif ($this->extension == 'pdo') {
			// PDO objects close their own connections when destroyed
		}
	}
	
	
	/**
	 * All requests that hit this method should be requests for callbacks
	 * 
	 * @internal
	 * 
	 * @param  string $method  The method to create a callback for
	 * @return callback  The callback for the method requested
	 */
	public function __get($method)
	{
		return array($this, $method);		
	}
	
	
	/**
	 * Checks to see if an SQL error occured
	 * 
	 * @param  fResult|fUnbufferedResult $result      The result object for the query
	 * @param  mixed                     $extra_info  The sqlite extension will pass a string error message, the oci8 extension will pass the statement resource
	 * @return void
	 */
	private function checkForError($result, $extra_info=NULL)
	{
		if ($result->getResult() === FALSE) {
			
			if ($this->extension == 'mssql') {
				$message = mssql_get_last_message();
			} elseif ($this->extension == 'mysql') {
				$message = mysql_error($this->connection);
			} elseif ($this->extension == 'mysqli') {
				$message = mysqli_error($this->connection);
			} elseif ($this->extension == 'oci8') {
				$error_info = oci_error($extra_info);
				$message = $error_info['message'];
			} elseif ($this->extension == 'odbc') {
				$message = odbc_errormsg($this->connection);
			} elseif ($this->extension == 'pgsql') {
				$message = pg_last_error($this->connection);
			} elseif ($this->extension == 'sqlite') {
				$message = $extra_info;
			} elseif ($this->extension == 'sqlsrv') {
				$error_info = sqlsrv_errors(SQLSRV_ERR_ALL);
				$message = $error_info[0]['message'];
			} elseif ($this->extension == 'pdo') {
				$error_info = $this->connection->errorInfo();
				$message = $error_info[2];
			}
			
			$db_type_map = array(
				'mssql'      => 'MSSQL',
				'mysql'      => 'MySQL',
				'oracle'     => 'Oracle',
				'postgresql' => 'PostgreSQL',
				'sqlite'     => 'SQLite'
			);
			
			throw new fSQLException(
				'%1$s error (%2$s) in %3$s',
				$db_type_map[$this->type],
				$message,
				$result->getSQL()
			);
		}
	}
	
	
	/**
	 * Clears all of the schema info out of the object and, if set, the fCache object
	 * 
	 * @return void
	 */
	public function clearCache()
	{
		$this->schema_info = array();
		if ($this->cache) {
			$this->cache->delete($this->makeCachePrefix() . 'schema_info');
		}
		if ($this->type == 'mssql') {
			$this->determineCharacterSet();		
		}
	}
	
	
	/**
	 * Connects to the database specified if no connection exists
	 * 
	 * @return void
	 */
	private function connectToDatabase()
	{
		// Don't try to reconnect if we are already connected
		if ($this->connection) { return; }
		
		// Establish a connection to the database
		if ($this->extension == 'pdo') {
			$odbc = strtolower(substr($this->database, 0, 4)) == 'dsn:';
			if ($this->type == 'mssql') {
				if ($odbc) {
					$dsn = 'odbc:' . substr($this->database, 4);
				} else {
					$separator = (fCore::checkOS('windows')) ? ',' : ':';
					$port      = ($this->port) ? $separator . $this->port : '';
					$driver    = (fCore::checkOs('windows')) ? 'mssql' : 'dblib';
					$dsn = $driver . ':host=' . $this->host . $port . ';dbname=' . $this->database;
				}
				
			} elseif ($this->type == 'mysql') {
				if (substr($this->host, 0, 5) == 'sock:') {
					$dsn = 'mysql:unix_socket=' . substr($this->host, 5) . ';dbname=' . $this->database;	
				} else {
					$port = ($this->port) ? ';port=' . $this->port : '';
					$dsn  = 'mysql:host=' . $this->host . ';dbname=' . $this->database . $port;
				}
				
			} elseif ($this->type == 'oracle') {
				if ($odbc) {
					$dsn = 'odbc:' . substr($this->database, 4);
				} else {
					$port = ($this->port) ? ':' . $this->port : '';
					$dsn  = 'oci:dbname=' . $this->host . $port . '/' . $this->database . ';charset=AL32UTF8';
				}
				
			} elseif ($this->type == 'postgresql') {
				
				$dsn = 'pgsql:dbname=' . $this->database;
				if ($this->host && $this->host != 'sock:') {
					$dsn .= ' host=' . $this->host;	
				}
				if ($this->port) {
					$dsn .= ' port=' . $this->port;	
				}
				
			} elseif ($this->type == 'sqlite') {
				$dsn = 'sqlite:' . $this->database;
			}
			
			try {
				$this->connection = new PDO($dsn, $this->username, $this->password);	
				if ($this->type == 'mysql') {
					$this->connection->setAttribute(PDO::MYSQL_ATTR_DIRECT_QUERY, 1);	
				}
			} catch (PDOException $e) {
				$this->connection = FALSE;
			}
		}
		
		if ($this->extension == 'sqlite') {
			$this->connection = sqlite_open($this->database);
		}
		
		if ($this->extension == 'mssql') {
			$separator        = (fCore::checkOS('windows')) ? ',' : ':';
			$this->connection = mssql_connect(($this->port) ? $this->host . $separator . $this->port : $this->host, $this->username, $this->password);
			if ($this->connection !== FALSE && mssql_select_db($this->database, $this->connection) === FALSE) {
				$this->connection = FALSE;
			}
		}
		
		if ($this->extension == 'mysql') {
			if (substr($this->host, 0, 5) == 'sock:') {
				$host = substr($this->host, 4);
			} elseif ($this->port) {
				$host = $this->host . ':' . $this->port;	
			} else {
				$host = $this->host;	
			}
			$this->connection = mysql_connect($host, $this->username, $this->password);
			if ($this->connection !== FALSE && mysql_select_db($this->database, $this->connection) === FALSE) {
				$this->connection = FALSE;
			}
		}
			
		if ($this->extension == 'mysqli') {
			if (substr($this->host, 0, 5) == 'sock:') {
				$this->connection = mysqli_connect('localhost', $this->username, $this->password, $this->database, $this->port, substr($this->host, 5));
			} elseif ($this->port) {
				$this->connection = mysqli_connect($this->host, $this->username, $this->password, $this->database, $this->port);
			} else {
				$this->connection = mysqli_connect($this->host, $this->username, $this->password, $this->database);
			}
		}
		
		if ($this->extension == 'oci8') {
			$this->connection = oci_connect($this->username, $this->password, $this->host . ($this->port ? ':' . $this->port : '') . '/' . $this->database, 'AL32UTF8');
		}
		
		if ($this->extension == 'odbc') {
			$this->connection = odbc_connect(substr($this->database, 4), $this->username, $this->password);
		}
			
		if ($this->extension == 'pgsql') {
			$connection_string = "dbname='" . addslashes($this->database) . "'";
			if ($this->host && $this->host != 'sock:') {
				$connection_string .= " host='" . addslashes($this->host) . "'";	
			}
			if ($this->username) {
				$connection_string .= " user='" . addslashes($this->username) . "'";
			}
			if ($this->password) {
				$connection_string .= " password='" . addslashes($this->password) . "'";
			}
			if ($this->port) {
				$connection_string .= " port='" . $this->port . "'";
			}
			$this->connection = pg_connect($connection_string);
		}
		
		if ($this->extension == 'sqlsrv') {
			$options = array(
				'Database' => $this->database,
				'UID'      => $this->username,
				'PWD'      => $this->password
			);
			$this->connection = sqlsrv_connect($this->host, $options);
		}
		
		// Ensure the connection was established
		if ($this->connection === FALSE) {
			throw new fConnectivityException(
				'Unable to connect to database'
			);
		}
		
		// Make MySQL act more strict and use UTF-8
		if ($this->type == 'mysql') {
			$this->query("SET SQL_MODE = 'REAL_AS_FLOAT,PIPES_AS_CONCAT,ANSI_QUOTES,IGNORE_SPACE'");
			$this->query("SET NAMES 'utf8'");
			$this->query("SET CHARACTER SET utf8");
		}
		
		// Make SQLite behave like other DBs for assoc arrays
		if ($this->type == 'sqlite') {
			$this->query('PRAGMA short_column_names = 1');
		}
		
		// Fix some issues with mssql
		if ($this->type == 'mssql') {
			if (!isset($this->schema_info['character_set'])) {
				$this->determineCharacterSet();
			}
			$this->query('SET TEXTSIZE 65536');
		}
		
		// Make PostgreSQL use UTF-8
		if ($this->type == 'postgresql') {
			$this->query("SET NAMES 'UTF8'");
		}
		
		// Oracle has different date and timestamp defaults
		if ($this->type == 'oracle') {
			$this->query("ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD'");
			$this->query("ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'");
			$this->query("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS TZR'");
			$this->query("ALTER SESSION SET NLS_TIME_FORMAT = 'HH24:MI:SS'");
			$this->query("ALTER SESSION SET NLS_TIME_TZ_FORMAT = 'HH24:MI:SS TZR'");
		}
	}
	
	
	/**
	 * Determines the character set of a SQL Server database
	 * 
	 * @return void
	 */
	protected function determineCharacterSet()
	{
		$this->schema_info['character_set'] = 'WINDOWS-1252';
		$this->schema_info['character_set'] = $this->query("SELECT 'WINDOWS-' + CONVERT(VARCHAR, COLLATIONPROPERTY(CONVERT(NVARCHAR, DATABASEPROPERTYEX(DB_NAME(), 'Collation')), 'CodePage')) AS charset")->fetchScalar();
		if ($this->cache) {
			$this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);	
		}
	}
	
	
	/**
	 * Figures out which extension to use for the database type selected
	 * 
	 * @return void
	 */
	protected function determineExtension()
	{
		switch ($this->type) {
			
			case 'mssql':
			
				$odbc = strtolower(substr($this->database, 0, 4)) == 'dsn:';
				
				if ($odbc) {
					if (extension_loaded('odbc')) {
						$this->extension = 'odbc';
						
					} elseif (class_exists('PDO', FALSE) && in_array('odbc', PDO::getAvailableDrivers())) {
						$this->extension = 'pdo';
						
					} else {
						$type = 'MSSQL (ODBC)';
						$exts = 'odbc, pdo_odbc';
					}
					
				} else {
					if (extension_loaded('sqlsrv')) {
						$this->extension = 'sqlsrv';
						
					} elseif (extension_loaded('mssql')) {
						$this->extension = 'mssql';
						
					} elseif (class_exists('PDO', FALSE) && (in_array('dblib', PDO::getAvailableDrivers()) || in_array('mssql', PDO::getAvailableDrivers()))) {
						$this->extension = 'pdo';
						
					} else {
						$type = 'MSSQL';
						$exts = 'mssql, sqlsrv, pdo_dblib (linux), pdo_mssql (windows)';
					}
				}
				break;
			
			
			case 'mysql':
			
				if (extension_loaded('mysql')) {
					$this->extension = 'mysql';
					
				} elseif (class_exists('PDO', FALSE) && in_array('mysql', PDO::getAvailableDrivers())) {
					$this->extension = 'pdo';
					
				} elseif (extension_loaded('mysqli')) {
					$this->extension = 'mysqli';
					
				} else {
					$type = 'MySQL';
					$exts = 'mysql, pdo_mysql, mysqli';
				}
				break;
				
				
			case 'oracle':
			
				$odbc = strtolower(substr($this->database, 0, 4)) == 'dsn:';
				
				if ($odbc) {
					if (extension_loaded('odbc')) {
						$this->extension = 'odbc';
						
					} elseif (class_exists('PDO', FALSE) && in_array('odbc', PDO::getAvailableDrivers())) {
						$this->extension = 'pdo';
						
					} else {
						$type = 'Oracle (ODBC)';
						$exts = 'odbc, pdo_odbc';
					}
					
				} else {
					if (extension_loaded('oci8')) {
						$this->extension = 'oci8';
						
					} elseif (class_exists('PDO', FALSE) && in_array('oci', PDO::getAvailableDrivers())) {
						$this->extension = 'pdo';
						
					} else {
						$type = 'Oracle';
						$exts = 'oci8, pdo_oci';
					}
				}
				break;
			
			
			case 'postgresql':
			
				if (extension_loaded('pgsql')) {
					$this->extension = 'pgsql';
					
				} elseif (class_exists('PDO', FALSE) && in_array('pgsql', PDO::getAvailableDrivers())) {
					$this->extension = 'pdo';
					
				} else {
					$type = 'PostgreSQL';
					$exts = 'pgsql, pdo_pgsql';
				}
				break;
				
				
			case 'sqlite':
			
				$sqlite_version = 0;
				
				if (file_exists($this->database)) {
					
					$database_handle  = fopen($this->database, 'r');
					$database_version = fread($database_handle, 64);
					fclose($database_handle);
					
					if (strpos($database_version, 'SQLite format 3') !== FALSE) {
						$sqlite_version = 3;
					} elseif (strpos($database_version, '** This file contains an SQLite 2.1 database **') !== FALSE) {
						$sqlite_version = 2;
					} else {
						throw new fConnectivityException(
							'The database specified does not appear to be a valid %1$s or %2$s database',
							'SQLite v2.1',
							'v3'
						);
					}
				}
				
				if ((!$sqlite_version || $sqlite_version == 3) && class_exists('PDO', FALSE) && in_array('sqlite', PDO::getAvailableDrivers())) {
					$this->extension = 'pdo';
					
				} elseif ($sqlite_version == 3 && (!class_exists('PDO', FALSE) || !in_array('sqlite', PDO::getAvailableDrivers()))) {
					throw new fEnvironmentException(
						'The database specified is an %1$s database and the %2$s extension is not installed',
						'SQLite v3',
						'pdo_sqlite'
					);
				
				} elseif ((!$sqlite_version || $sqlite_version == 2) && extension_loaded('sqlite')) {
					$this->extension = 'sqlite';
					
				} elseif ($sqlite_version == 2 && !extension_loaded('sqlite')) {
					throw new fEnvironmentException(
						'The database specified is an %1$s database and the %2$s extension is not installed',
						'SQLite v2.1',
						'sqlite'
					);
				
				} else {
					$type = 'SQLite';
					$exts = 'pdo_sqlite, sqlite';
				}
				break;
		}
		
		if (!$this->extension) {
			throw new fEnvironmentException(
				'The server does not have any of the following extensions for %2$s support: %2$s',
				$type,
				$exts
			);
		}
	}
	
	
	/**
	 * Sets the schema info to be cached to the fCache object specified
	 * 
	 * @param  fCache $cache  The cache to cache to
	 * @return void
	 */
	public function enableCaching($cache)
	{
		$this->cache = $cache;
		
		$this->schema_info = $this->cache->get($this->makeCachePrefix() . 'schema_info', array());
	}
	
	
	/**
	 * Sets if debug messages should be shown
	 * 
	 * @param  boolean $flag  If debugging messages should be shown
	 * @return void
	 */
	public function enableDebugging($flag)
	{
		$this->debug = (boolean) $flag;
	}
	
	
	/**
	 * Sets a flag to trigger a PHP warning message whenever a query takes longer than the millisecond threshold specified
	 * 
	 * It is recommended to use the error handling features of
	 * fCore::enableErrorHandling() to log or email these warnings.
	 * 
	 * @param  integer $threshold  The limit (in milliseconds) of how long an SQL query can take before a warning is triggered
	 * @return void
	 */
	public function enableSlowQueryWarnings($threshold)
	{
		$this->slow_query_threshold = (int) $threshold;
	}
	
	
	/**
	 * Escapes a value for insertion into SQL
	 * 
	 * The valid data types are:
	 * 
	 *  - `'blob'`
	 *  - `'boolean'`
	 *  - `'date'`
	 *  - `'float'`
	 *  - `'integer'`
	 *  - `'string'` (also varchar, char or text)
	 *  - `'varchar'`
	 *  - `'char'`
	 *  - `'text'`
	 *  - `'time'`
	 *  - `'timestamp'`
	 * 
	 * In addition to being able to specify the data type, you can also pass
	 * in an SQL statement with data type placeholders in the following form:
	 *   
	 *  - `%l` for a blob
	 *  - `%b` for a boolean
	 *  - `%d` for a date
	 *  - `%f` for a float
	 *  - `%i` for an integer
	 *  - `%s` for a string
	 *  - `%t` for a time
	 *  - `%p` for a timestamp
	 * 
	 * Depending on what `$sql_or_type` and `$value` are, the output will be
	 * slightly different. If `$sql_or_type` is a data type or a single
	 * placeholder and `$value` is:
	 * 
	 *  - a scalar value - an escaped SQL string is returned
	 *  - an array - an array of escaped SQL strings is returned
	 * 
	 * If `$sql_or_type` is a SQL string and `$value` is:
	 * 
	 *  - a scalar value - the escaped value is inserted into the SQL string
	 *  - an array - the escaped values are inserted into the SQL string separated by commas
	 * 
	 * If `$sql_or_type` is a SQL string, it is also possible to pass an array
	 * of all values as a single parameter instead of one value per parameter.
	 * An example would look like the following:
	 * 
	 * {{{
	 * #!php
	 * $db->escape(
	 *     "SELECT * FROM users WHERE status = %s AND authorization_level = %s",
	 *     array('Active', 'Admin')
	 * );
	 * }}}
	 * 
	 * @param  string $sql_or_type  This can either be the data type to escape or an SQL string with a data type placeholder - see method description
	 * @param  mixed  $value        The value to escape - both single values and arrays of values are supported, see method description for details
	 * @param  mixed  ...
	 * @return mixed  The escaped value/SQL or an array of the escaped values
	 */
	public function escape($sql_or_type, $value)
	{
		$values = array_slice(func_get_args(), 1);
		
		if (sizeof($values) < 1) {
			throw new fProgrammerException(
				'No value was specified to escape'
			);	
		}
		
		// Convert all objects into strings
		$values = $this->scalarize($values);
		
		$value = array_shift($values);
		
		// Handle single value escaping
		$callback = NULL;
		
		switch ($sql_or_type) {
			case 'blob':
			case '%l':
				$callback = $this->escapeBlob;
				break;
			case 'boolean':
			case '%b':
				$callback = $this->escapeBoolean;
				break;
			case 'date':
			case '%d':
				$callback = $this->escapeDate;
				break;
			case 'float':
			case '%f':
				$callback = $this->escapeFloat;
				break;
			case 'integer':
			case '%i':
				$callback = $this->escapeInteger;
				break;
			case 'string':
			case 'varchar':
			case 'char':
			case 'text':
			case '%s':
				$callback = $this->escapeString;
				break;
			case 'time':
			case '%t':
				$callback = $this->escapeTime;
				break;
			case 'timestamp':
			case '%p':
				$callback = $this->escapeTimestamp;
				break;
		}
		
		if ($callback) {
			if (is_array($value)) {
				// If the values were passed as a single array, this handles that
				if (count($value) == 1 && is_array(current($value))) {
					$value = current($value);
				}
				return array_map($callback, $value);		
			}
			return call_user_func($callback, $value);
		}	
		
		// Fix \' in MySQL and PostgreSQL
		if(($this->type == 'mysql' || $this->type == 'postgresql') && strpos($sql_or_type, '\\') !== FALSE) {
			$sql_or_type = preg_replace("#(?<!\\\\)((\\\\{2})*)\\\\'#", "\\1''", $sql_or_type);	
		}
		
		// Separate the SQL from quoted values
		preg_match_all("#(?:'([^']*(?:'')*)*?')|(?:[^']+)#", $sql_or_type, $matches);
		
		$temp_sql = '';
		$strings = array();
		
		// Replace strings with a placeholder so they don't mess use the regex parsing
		foreach ($matches[0] as $match) {
			if ($match[0] == "'") {
				$strings[] = $match;
				$match = ':string_' . (sizeof($strings)-1);
			}
			$temp_sql .= $match;
		}
		
		$pieces = preg_split('#(%[lbdfistp])\b#', $temp_sql, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
		$sql = '';
		
		// If the values were passed as a single array, this handles that
		if (count($values) == 0 && is_array($value)) {
			$placeholders = 0;
			foreach ($pieces as $piece) {
				if (strlen($piece) == 2 && $piece[0] == '%') {
					$placeholders++;
				}	
			}
			
			if ($placeholders == count($value)) {
				$values = $value;
				$value  = array_shift($values);	
			}
		}
		
		$missing_values = -1;
		
		foreach ($pieces as $piece) {
			switch ($piece) {
				case '%l':
					$callback = $this->escapeBlob;
					break;
				case '%b':
					$callback = $this->escapeBoolean;
					break;
				case '%d':
					$callback = $this->escapeDate;
					break;
				case '%f':
					$callback = $this->escapeFloat;
					break;
				case '%i':
					$callback = $this->escapeInteger;
					break;
				case '%s':
					$callback = $this->escapeString;
					break;
				case '%t':
					$callback = $this->escapeTime;
					break;
				case '%p':
					$callback = $this->escapeTimestamp;
					break;
				default:
					$sql .= $piece;
					continue 2;	
			}
			
			if (is_array($value)) {
				$sql .= join(', ', array_map($callback, $value));		
			} else {
				$sql .= call_user_func($callback, $value);
			}
					
			if (sizeof($values)) {
				$value = array_shift($values);
			} else {
				$value = NULL;
				$missing_values++;	
			}
		}
		
		if ($missing_values > 0) {
			throw new fProgrammerException(
				'%1$s value(s) are missing for the placeholders in: %2$s',
				$missing_values,
				$sql_or_type
			);	
		}
		
		if (sizeof($values)) {
			throw new fProgrammerException(
				'%1$s extra value(s) were passed for the placeholders in: %2$s',
				sizeof($values),
				$sql_or_type
			); 	
		}
		
		$string_number = 0;
		foreach ($strings as $string) {
			$string = strtr($string, array('\\' => '\\\\', '$' => '\\$'));
			$sql    = preg_replace('#:string_' . $string_number++ . '\b#', $string, $sql);	
		}
		
		return $sql;
	}
	
	
	/**
	 * Escapes a blob for use in SQL, includes surround quotes when appropriate
	 * 
	 * A `NULL` value will be returned as `'NULL'`
	 * 
	 * @param  string $value  The blob to escape
	 * @return string  The escaped blob
	 */
	private function escapeBlob($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		
		$this->connectToDatabase();
		
		if ($this->type == 'mysql') {
			return "x'" . bin2hex($value) . "'";
			
		} elseif ($this->type == 'postgresql') {
			$output = '';
			for ($i=0; $i<strlen($value); $i++) {
				$output .= '\\\\' . str_pad(decoct(ord($value[$i])), 3, '0', STR_PAD_LEFT);
			}
			return "E'" . $output . "'";
			
		} elseif ($this->extension == 'sqlite') {
			return "'" . bin2hex($value) . "'";
			
		} elseif ($this->type == 'sqlite') {
			return "X'" . bin2hex($value) . "'";
			
		} elseif ($this->type == 'mssql') {
			return '0x' . bin2hex($value);
			
		} elseif ($this->type == 'oracle') {
			return "'" . bin2hex($value) . "'";
		}
	}
	
	
	/**
	 * Escapes a boolean for use in SQL, includes surround quotes when appropriate
	 * 
	 * A `NULL` value will be returned as `'NULL'`
	 * 
	 * @param  boolean $value  The boolean to escape
	 * @return string  The database equivalent of the boolean passed
	 */
	private function escapeBoolean($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		
		if (in_array($this->type, array('postgresql', 'mysql'))) {
			return ($value) ? 'TRUE' : 'FALSE';
		} elseif (in_array($this->type, array('mssql', 'sqlite'))) {
			return ($value) ? "'1'" : "'0'";
		} elseif ($this->type == 'oracle') {
			return ($value) ? '1' : '0';	
		}
	}
	
	
	/**
	 * Escapes a date for use in SQL, includes surrounding quotes
	 * 
	 * A `NULL` or invalid value will be returned as `'NULL'`
	 * 
	 * @param  string $value  The date to escape
	 * @return string  The escaped date
	 */
	private function escapeDate($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		
		try {
			$value = new fDate($value);
			return "'" . $value->format('Y-m-d') . "'";
			
		} catch (fValidationException $e) {
			return 'NULL';
		}
	}
	
	
	/**
	 * Escapes a float for use in SQL
	 * 
	 * A `NULL` value will be returned as `'NULL'`
	 * 
	 * @param  float $value  The float to escape
	 * @return string  The escaped float
	 */
	private function escapeFloat($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		if (!strlen($value)) {
			return 'NULL';
		}
		if (!preg_match('#^[+\-]?([0-9]+(\.[0-9]+)?|(\.[0-9]+))$#D', $value)) {
			return 'NULL';
		}
		return (string) $value;
	}
	
	
	/**
	 * Escapes an integer for use in SQL
	 * 
	 * A `NULL` or invalid value will be returned as `'NULL'`
	 * 
	 * @param  integer $value  The integer to escape
	 * @return string  The escaped integer
	 */
	private function escapeInteger($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		if (!strlen($value)) {
			return 'NULL';
		}
		if (!preg_match('#^[+\-]?[0-9]+$#D', $value)) {
			return 'NULL';
		}
		return (string) $value;
	}
	
	
	/**
	 * Escapes a string for use in SQL, includes surrounding quotes
	 * 
	 * A `NULL` value will be returned as `'NULL'`
	 * 
	 * @param  string $value  The string to escape
	 * @return string  The escaped string
	 */
	private function escapeString($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		
		$this->connectToDatabase();
		
		if ($this->extension == 'mysql') {
			return "'" . mysql_real_escape_string($value, $this->connection) . "'";
		} elseif ($this->extension == 'mysqli') {
			return "'" . mysqli_real_escape_string($this->connection, $value) . "'";
		} elseif ($this->extension == 'pgsql') {
			return "'" . pg_escape_string($value) . "'";
		} elseif ($this->extension == 'sqlite') {
			return "'" . sqlite_escape_string($value) . "'";
		} elseif ($this->type == 'oracle') {
			return "'" . str_replace("'", "''", $value) . "'";
			
		} elseif ($this->type == 'mssql') {
			
			// If there are any non-ASCII characters, we need to escape
			if (preg_match('#[^\x00-\x7F]#', $value)) {
				preg_match_all('#.|^\z#us', $value, $characters);
				$output    = "";
				$last_type = NULL;
				foreach ($characters[0] as $character) {
					if (strlen($character) > 1) {
						$b = array_map('ord', str_split($character));
						switch (strlen($character)) {
							case 2:
								$bin = substr(decbin($b[0]), 3) .
										   substr(decbin($b[1]), 2);
								break;
							
							case 3:
								$bin = substr(decbin($b[0]), 4) .
										   substr(decbin($b[1]), 2) .
										   substr(decbin($b[2]), 2);
								break;
							
							// If it is a 4-byte character, MSSQL can't store it
							// so instead store a ?
							default:
								$output .= '?';
								continue;
						}
						if ($last_type == 'nchar') {
							$output .= '+';
						} elseif ($last_type == 'char') {
							$output .= "'+";
						}		
						$output .= "NCHAR(" . bindec($bin) . ")";
						$last_type = 'nchar';
					} else {
						if (!$last_type) {
							$output .= "'";
						} elseif ($last_type == 'nchar') {
							$output .= "+'";	
						}
						$output .= $character;
						// Escape single quotes
						if ($character == "'") {
							$output .= "'";
						}
						$last_type = 'char';
					}
				}
				if ($last_type == 'char') {
					$output .= "'";
				} elseif (!$last_type) {
					$output .= "''";	
				}
			
			// ASCII text is normal
			} else {
				$output = "'" . str_replace("'", "''", $value) . "'";
			}
			
			# a \ before a \r\n has to be escaped with another \
			return preg_replace('#(?<!\\\\)\\\\(?=\r\n)#', '\\\\\\\\', $output);
		
		} elseif ($this->extension == 'pdo') {
			return $this->connection->quote($value);
		}
	}
	
	
	/**
	 * Escapes a time for use in SQL, includes surrounding quotes
	 * 
	 * A `NULL` or invalid value will be returned as `'NULL'`
	 * 
	 * @param  string $value  The time to escape
	 * @return string  The escaped time
	 */
	private function escapeTime($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		
		try {
			$value = new fTime($value);
			
			if ($this->type == 'mssql' || $this->type == 'oracle') {
				return "'" . $value->format('1970-01-01 H:i:s') . "'";	
			}
			
			return "'" . $value->format('H:i:s') . "'";
			
		} catch (fValidationException $e) {
			return 'NULL';
		}
	}
	
	
	/**
	 * Escapes a timestamp for use in SQL, includes surrounding quotes
	 * 
	 * A `NULL` or invalid value will be returned as `'NULL'`
	 * 
	 * @param  string $value  The timestamp to escape
	 * @return string  The escaped timestamp
	 */
	private function escapeTimestamp($value)
	{
		if ($value === NULL) {
			return 'NULL';
		}
		
		try {
			$value = new fTimestamp($value);
			return "'" . $value->format('Y-m-d H:i:s') . "'";
			
		} catch (fValidationException $e) {
			return 'NULL';
		}
	}
	
	
	/**
	 * Executes an SQL query
	 * 
	 * @param  fResult $result  The result object for the query
	 * @return void
	 */
	private function executeQuery($result)
	{
		// We don't want errors and an exception
		$old_level = error_reporting(error_reporting() & ~E_WARNING);
		
		if ($this->extension == 'mssql') {
			$result->setResult(mssql_query($result->getSQL(), $this->connection));
			
		} elseif ($this->extension == 'mysql') {
			$result->setResult(mysql_query($result->getSQL(), $this->connection));
			
		} elseif ($this->extension == 'mysqli') {
			$result->setResult(mysqli_query($this->connection, $result->getSQL()));
			
		} elseif ($this->extension == 'oci8') {
			$oci_statement = oci_parse($this->connection, $result->getSQL());
			if (oci_execute($oci_statement, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS)) {
				oci_fetch_all($oci_statement, $rows, 0, -1, OCI_FETCHSTATEMENT_BY_ROW + OCI_ASSOC);
				$result->setResult($rows);
				unset($rows);	
			} else {
				$result->setResult(FALSE);
			}
			
		} elseif ($this->extension == 'odbc') {
			$resource = odbc_exec($this->connection, $result->getSQL());
			if (is_resource($resource)) {
				$rows = array();
				// Allow up to 1MB of binary data
				odbc_longreadlen($resource, 1048576);
				odbc_binmode($resource, ODBC_BINMODE_CONVERT);
				while ($row = odbc_fetch_array($resource)) {
					$rows[] = $row;
				}
				$result->setResult($rows);
				unset($rows);
			} else {
				$result->setResult($resource);
			}
			
		} elseif ($this->extension == 'pgsql') {
			$result->setResult(pg_query($this->connection, $result->getSQL()));
			
		} elseif ($this->extension == 'sqlite') {
			$result->setResult(sqlite_query($this->connection, $result->getSQL(), SQLITE_ASSOC, $sqlite_error_message));
			
		} elseif ($this->extension == 'sqlsrv') {
			$resource = sqlsrv_query($this->connection, $result->getSQL());
			if (is_resource($resource)) {
				$rows = array();
				while ($row = sqlsrv_fetch_array($resource, SQLSRV_FETCH_ASSOC)) {
					$rows[] = $row;
				}
				$result->setResult($rows);
				unset($rows);
			} else {
				$result->setResult($resource);
			}
			
		} elseif ($this->extension == 'pdo') {
			if (preg_match('#^\s*CREATE(\s+OR\s+REPLACE)?\s+TRIGGER#i', $result->getSQL())) {
				$this->connection->exec($result->getSQL());
				$pdo_statement = FALSE;
				$returned_rows = array();
			} else {
				$pdo_statement = $this->connection->query($result->getSQL());
				$returned_rows = (is_object($pdo_statement)) ? $pdo_statement->fetchAll(PDO::FETCH_ASSOC) : $pdo_statement;
				
				// The pdo_pgsql driver likes to return empty rows equal to the number of affected rows for insert and deletes
				if ($this->type == 'postgresql' && $returned_rows && $returned_rows[0] == array()) {
					$returned_rows = array(); 		
				}
			}
			
			$result->setResult($returned_rows);
		}
		
		error_reporting($old_level);
		
		
		if ($this->extension == 'sqlite') {
			$this->checkForError($result, $sqlite_error_message);
		} elseif ($this->extension == 'oci8') {
			$this->checkForError($result, $oci_statement);
		} else {
			$this->checkForError($result);
		}
		
		
		if ($this->extension == 'pdo') {
			$this->setAffectedRows($result, $pdo_statement);
			if ($pdo_statement) {
				$pdo_statement->closeCursor();
			}
			unset($pdo_statement);
		} elseif ($this->extension == 'oci8') {
			$this->setAffectedRows($result, $oci_statement);
			oci_free_statement($oci_statement);
		} elseif ($this->extension == 'odbc') {
			$this->setAffectedRows($result, $resource);
			odbc_free_result($resource);
		} elseif ($this->extension == 'sqlsrv') {
			$this->setAffectedRows($result, $resource);
			sqlsrv_free_stmt($resource);
		} else {
			$this->setAffectedRows($result);
		}
		
		$this->setReturnedRows($result);
		
		$this->handleAutoIncrementedValue($result);
	}
	
	
	/**
	 * Executes an unbuffered SQL query
	 * 
	 * @param  fUnbufferedResult $result  The result object for the query
	 * @return void
	 */
	private function executeUnbufferedQuery($result)
	{
		$old_level = error_reporting(error_reporting() & ~E_WARNING);
		
		if ($this->extension == 'mssql') {
			$result->setResult(mssql_query($result->getSQL(), $this->connection, 20));
		} elseif ($this->extension == 'mysql') {
			$result->setResult(mysql_unbuffered_query($result->getSQL(), $this->connection));
		} elseif ($this->extension == 'mysqli') { 
			$result->setResult(mysqli_query($this->connection, $result->getSQL(), MYSQLI_USE_RESULT));
		} elseif ($this->extension == 'oci8') {
			$oci_statement = oci_parse($this->connection, $result->getSQL());
			$result->setResult(oci_execute($oci_statement, $this->inside_transaction ? OCI_DEFAULT : OCI_COMMIT_ON_SUCCESS) ? $oci_statement : FALSE);
		} elseif ($this->extension == 'odbc') {
			$result->setResult(odbc_exec($this->connection, $result->getSQL()));
		} elseif ($this->extension == 'pgsql') {
			$result->setResult(pg_query($this->connection, $result->getSQL()));
		} elseif ($this->extension == 'sqlite') {
			$result->setResult(sqlite_unbuffered_query($this->connection, $result->getSQL(), SQLITE_ASSOC, $sqlite_error_message));
		} elseif ($this->extension == 'sqlsrv') {
			$result->setResult(sqlsrv_query($this->connection, $result->getSQL()));
		} elseif ($this->extension == 'pdo') {
			$result->setResult($this->connection->query($result->getSQL()));
		}
		
		error_reporting($old_level);
		
		if ($this->extension == 'sqlite') {
			$this->checkForError($result, $sqlite_error_message);
		} elseif ($this->extension == 'oci8') {
			$this->checkForError($result, $oci_statement);
		} else {
			$this->checkForError($result);
		}
	}
	
	
	/**
	 * Takes in a string of SQL that contains multiple queries and returns any array of them
	 * 
	 * @param  string $sql  The string of SQL to parse for queries
	 * @return array  The individual SQL queries
	 */
	private function explodeQueries($sql)
	{
		$sql_queries = array();
		
		// Separate the SQL from quoted values
		preg_match_all("#(?:'([^']*(?:'')*)*?')|(?:[^']+)#", $sql, $matches);
		
		$cur_sql = '';
		foreach ($matches[0] as $match) {
			
			// This is a quoted string value, don't do anything to it
			if ($match[0] == "'") {
				$cur_sql .= $match;
			
			// Handle the SQL, exploding on any ; that isn't escaped with a \
			} else {
				$sql_strings = preg_split('#(?<!\\\\);#', $match);
				$cur_sql .= $sql_strings[0];
				for ($i=1; $i < sizeof($sql_strings); $i++) {
					$cur_sql = trim($cur_sql);
					if ($cur_sql) {
						$sql_queries[] = $cur_sql;
					}
					$cur_sql = $sql_strings[$i];
				}
			}
		}
		if (trim($cur_sql)) {
			$sql_queries[] = $cur_sql;
		}
		
		return $sql_queries;
	}
	
	
	/**
	 * Returns the database connection resource or object
	 * 
	 * @return mixed  The database connection
	 */
	public function getConnection()
	{
		$this->connectToDatabase();
		return $this->connection;
	}
	
	
	/**
	 * Gets the name of the database currently connected to
	 * 
	 * @return string  The name of the database currently connected to
	 */
	public function getDatabase()
	{
		return $this->database;
	}
	
	
	/**
	 * Gets the php extension being used
	 * 
	 * @internal
	 * 
	 * @return string  The php extension used for database interaction
	 */
	public function getExtension()
	{
		return $this->extension;
	}
	
	
	/**
	 * Gets the host for this database
	 * 
	 * @return string  The host
	 */
	public function getHost()
	{
		return $this->host;
	}
	
	
	/**
	 * Gets the port for this database
	 * 
	 * @return string  The port
	 */
	public function getPort()
	{
		return $this->port;
	}
	
	
	/**
	 * Gets the fSQLTranslation object used for translated queries
	 * 
	 * @return fSQLTranslation  The SQL translation object
	 */
	public function getSQLTranslation()
	{
		if (!$this->translation) { new fSQLTranslation($this); }
		return $this->translation;	
	}
	
	
	/**
	 * Gets the database type
	 * 
	 * @return string  The database type: `'mssql'`, `'mysql'`, `'postgresql'` or `'sqlite'`
	 */
	public function getType()
	{
		return $this->type;
	}
	
	
	/**
	 * Gets the username for this database
	 * 
	 * @return string  The username
	 */
	public function getUsername()
	{
		return $this->username;
	}
	
	
	/**
	 * Will grab the auto incremented value from the last query (if one exists)
	 * 
	 * @param  fResult $result  The result object for the query
	 * @return void
	 */
	private function handleAutoIncrementedValue($result)
	{
		if (!preg_match('#^\s*INSERT\s+INTO\s+(?:`|"|\[)?(\w+)(?:`|"|\])?#i', $result->getSQL(), $table_match)) {
			$result->setAutoIncrementedValue(NULL);
			return;
		}
		$table = strtolower($table_match[1]);
		
		$insert_id = NULL;
		
		if ($this->type == 'oracle') {
			if (!isset($this->schema_info['sequences'])) {
				$sql = "SELECT
								TABLE_NAME,
								TRIGGER_BODY
							FROM
								USER_TRIGGERS
							WHERE
								TRIGGERING_EVENT = 'INSERT' AND
								STATUS = 'ENABLED' AND
								TRIGGER_NAME NOT LIKE 'BIN\$%'";
								
				$this->schema_info['sequences'] = array();
				
				foreach ($this->query($sql) as $row) {
					if (preg_match('#SELECT\s+(\w+).nextval\s+INTO\s+:new\.(\w+)\s+FROM\s+dual#i', $row['trigger_body'], $matches)) {
						$this->schema_info['sequences'][strtolower($row['table_name'])] = array('sequence' => $matches[1], 'column' => $matches[2]);
					}
				}
				
				if ($this->cache) {
					$this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);	
				}
			}
			
			if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+' . preg_quote($table, '#') . '\s+\([^\)]*?\b' . preg_quote($this->schema_info['sequences'][$table]['column'], '#') . '\b#i', $result->getSQL())) {
				return;	
			}
			
			$insert_id_sql = "SELECT " . $this->schema_info['sequences'][$table]['sequence'] . ".currval AS INSERT_ID FROM dual";
		}
		
		if ($this->type == 'postgresql') {
			if (!isset($this->schema_info['sequences'])) {
				$sql = "SELECT
								pg_class.relname AS table_name,
								pg_attribute.attname AS column
							FROM
								pg_attribute INNER JOIN
								pg_class ON pg_attribute.attrelid = pg_class.oid INNER JOIN
								pg_attrdef ON pg_class.oid = pg_attrdef.adrelid AND pg_attribute.attnum = pg_attrdef.adnum
							WHERE
								NOT pg_attribute.attisdropped AND
								pg_attrdef.adsrc LIKE 'nextval(%'";
								
				$this->schema_info['sequences'] = array();
				
				foreach ($this->query($sql) as $row) {
					$this->schema_info['sequences'][strtolower($row['table_name'])] = $row['column'];
				}
				
				if ($this->cache) {
					$this->cache->set($this->makeCachePrefix() . 'schema_info', $this->schema_info);	
				}	
			}
			
			if (!isset($this->schema_info['sequences'][$table]) || preg_match('#INSERT\s+INTO\s+' . preg_quote($table, '#') . '\s+\([^\)]*?\b' . preg_quote($this->schema_info['sequences'][$table], '#') . '\b#i', $result->getSQL())) {
				return;
			} 		
		}
		
		if ($this->extension == 'mssql') {
			$insert_id_res = mssql_query("SELECT @@IDENTITY AS insert_id", $this->connection);
			$insert_id     = mssql_result($insert_id_res, 0, 'insert_id');
			mssql_free_result($insert_id_res);
		
		} elseif ($this->extension == 'mysql') {
			$insert_id     = mysql_insert_id($this->connection);
		
		} elseif ($this->extension == 'mysqli') {
			$insert_id     = mysqli_insert_id($this->connection);
		
		} elseif ($this->extension == 'oci8') {
			$oci_statement = oci_parse($this->connection, $insert_id_sql);
			oci_execute($oci_statement);
			$insert_id_row = oci_fetch_array($oci_statement, OCI_ASSOC);
			$insert_id = $insert_id_row['INSERT_ID'];
			oci_free_statement($oci_statement);
		
		} elseif ($this->extension == 'odbc' && $this->type == 'mssql') {
			$insert_id_res = odbc_exec($this->connection, "SELECT @@IDENTITY AS insert_id");
			$insert_id     = odbc_result($insert_id_res, 'insert_id');
			odbc_free_result($insert_id_res);
		
		} elseif ($this->extension == 'odbc' && $this->type == 'oracle') {
			$insert_id_res = odbc_exec($this->connection, $insert_id_sql);
			$insert_id     = odbc_result($insert_id_res, 'insert_id');
			odbc_free_result($insert_id_res);
		
		} elseif ($this->extension == 'pgsql') {
			
			$insert_id_res = pg_query($this->connection, "SELECT lastval()");
			$insert_id_row = pg_fetch_assoc($insert_id_res);
			$insert_id = array_shift($insert_id_row);
			pg_free_result($insert_id_res);
		
		} elseif ($this->extension == 'sqlite') {
			$insert_id = sqlite_last_insert_rowid($this->connection);
		
		} elseif ($this->extension == 'sqlsrv') {
			$insert_id_res = sqlsrv_query($this->connection, "SELECT @@IDENTITY AS insert_id");
			$insert_id_row = sqlsrv_fetch_array($insert_id_res, SQLSRV_FETCH_ASSOC);
			$insert_id     = $insert_id_row['insert_id'];
			sqlsrv_free_stmt($insert_id_res);
		
		} elseif ($this->extension == 'pdo') {
			
			switch ($this->type) {
				
				case 'mssql':
					try {
						$insert_id_statement = $this->connection->query("SELECT @@IDENTITY AS insert_id");
						if (!$insert_id_statement) {
							throw new Exception();
						}
						
						$insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
						$insert_id = array_shift($insert_id_row);
						
					} catch (Exception $e) {
						// If there was an error we don't have an insert id
					}
					break;
					
				case 'oracle':
					try {
						$insert_id_statement = $this->connection->query($insert_id_sql);
						if (!$insert_id_statement) {
							throw new Exception();
						}
						
						$insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
						$insert_id = array_shift($insert_id_row);
						
					} catch (Exception $e) {
						// If there was an error we don't have an insert id
					}
					break;
				
				case 'postgresql':
					
					$insert_id_statement = $this->connection->query("SELECT lastval()");
					$insert_id_row = $insert_id_statement->fetch(PDO::FETCH_ASSOC);
					$insert_id = array_shift($insert_id_row);
					$insert_id_statement->closeCursor();
					unset($insert_id_statement);
					
					break;
		
				case 'mysql':
					$insert_id = $this->connection->lastInsertId();
					break;
		
				case 'sqlite':
					$insert_id = $this->connection->lastInsertId();
					break;
			}
		}
		
		$result->setAutoIncrementedValue($insert_id);
	}
	
	
	/**
	 * Makes sure each database and extension handles BEGIN, COMMIT and ROLLBACK 
	 * 
	 * @param  string &$sql          The SQL to check for a transaction query
	 * @param  string $result_class  The type of result object to create
	 * @return mixed  `FALSE` if normal processing should continue, otherwise an object of the type $result_class
	 */
	private function handleTransactionQueries(&$sql, $result_class)
	{
		// SQL Server supports transactions, but starts then with BEGIN TRANSACTION
		if ($this->type == 'mssql' && preg_match('#^\s*(begin|start(\s+transaction)?)\s*#i', $sql)) {
			$sql = 'BEGIN TRANSACTION';
		}
		
		$begin    = FALSE;
		$commit   = FALSE;
		$rollback = FALSE;
		
		// Track transactions since most databases don't support nesting
		if (preg_match('#^\s*(begin|start)(\s+(transaction|work))?\s*$#iD', $sql)) {
			if ($this->inside_transaction) {
				throw new fProgrammerException('A transaction is already in progress');
			}
			$this->inside_transaction = TRUE;
			$begin = TRUE;
			
		} elseif (preg_match('#^\s*(commit)(\s+(transaction|work))?\s*$#iD', $sql)) {
			if (!$this->inside_transaction) {
				throw new fProgrammerException('There is no transaction in progress');
			}
			$this->inside_transaction = FALSE;
			$commit = TRUE;
			
		} elseif (preg_match('#^\s*(rollback)(\s+(transaction|work))?\s*$#iD', $sql)) {
			if (!$this->inside_transaction) {
				throw new fProgrammerException('There is no transaction in progress');
			}
			$this->inside_transaction = FALSE;
			$rollback = TRUE;
		}
		
		if (!$begin && !$commit && !$rollback) {
			return FALSE;	
		}
		
		// The PDO, OCI8, ODBC and SQLSRV extensions require special handling through methods and functions
		$is_pdo    = $this->extension == 'pdo';
		$is_oci    = $this->extension == 'oci8';
		$is_odbc   = $this->extension == 'odbc';
		$is_sqlsrv = $this->extension == 'sqlsrv';
		
		if (!$is_pdo && !$is_oci && !$is_odbc && !$is_sqlsrv) {
			return FALSE;
		}
		
		// PDO seems to act weird if you try to start transactions through a normal query call
		if ($is_pdo) {
			try {
				$is_mssql  = $this->type == 'mssql'  && substr($this->database, 0, 4) != 'dsn:';
				$is_oracle = $this->type == 'oracle' && substr($this->database, 0, 4) != 'dsn:';
				if ($begin) {
					// The SQL Server PDO object hasn't implemented transactions
					if ($is_mssql) {
						$this->connection->exec('BEGIN TRANSACTION');
					} elseif ($is_oracle) {
						$this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, FALSE);
					} else {
						$this->connection->beginTransaction();
					}
				
				} elseif ($commit) {
					if ($is_mssql) {
						$this->connection->exec('COMMIT');
					} elseif ($is_oracle) {
						$this->connection->exec('COMMIT');
						$this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE);
					} else  {
						$this->connection->commit();
					}
				
				} elseif ($rollback) {
					if ($is_mssql) {
						$this->connection->exec('ROLLBACK');
					} elseif ($is_oracle) {                 
						$this->connection->exec('ROLLBACK');
						$this->connection->setAttribute(PDO::ATTR_AUTOCOMMIT, TRUE);
					} else {
						$this->connection->rollBack();
					}
				}
				
			} catch (Exception $e) {
				
				$db_type_map = array(
					'mssql'      => 'MSSQL',
					'mysql'      => 'MySQL',
					'oracle'     => 'Oracle',
					'postgresql' => 'PostgreSQL',
					'sqlite'     => 'SQLite'
				);
				
				throw new fSQLException(
					'%1$s error (%2$s) in %3$s',
					$db_type_map[$this->type],
					$e->getMessage(),
					$sql
				);
			}
		
		} elseif ($is_oci) {
			if ($commit) {
				oci_commit($this->connection);
			} elseif ($rollback) {
				oci_rollback($this->connection);
			}
		
		} elseif ($is_odbc) {
			if ($begin) {
				odbc_autocommit($this->connection, FALSE);
			} elseif ($commit) {
				odbc_commit($this->connection);
				odbc_autocommit($this->connection, TRUE);
			} elseif ($rollback) {
				odbc_rollback($this->connection);
				odbc_autocommit($this->connection, TRUE);
			}
			
		} elseif ($is_sqlsrv) {
			if ($begin) {
				sqlsrv_begin_transaction($this->connection);
			} elseif ($commit) {
				sqlsrv_commit($this->connection);
			} elseif ($rollback) {
				sqlsrv_rollback($this->connection);
			}	
		}
		
		$result = new $result_class($this);
		$result->setSQL($sql);
		$result->setResult(TRUE);
		return $result;
	}
	
	
	/**
	 * Injects an fSQLTranslation object to handle translation
	 * 
	 * @internal
	 * 
	 * @param  fSQLTranslation $sql_translation  The SQL translation object
	 * @return void
	 */
	public function inject($sql_translation)
	{
		$this->translation = $sql_translation;
	}
	
	
	/**
	 * Will indicate if a transaction is currently in progress
	 * 
	 * @return boolean  If a transaction has been started and not yet rolled back or committed
	 */
	public function isInsideTransaction()
	{
		return $this->inside_transaction;
	}
	
	
	/**
	 * Creates a unique cache prefix to help prevent cache conflicts
	 * 
	 * @return void
	 */
	private function makeCachePrefix()
	{
		$prefix  = 'fDatabase::' . $this->type . '::';
		if ($this->host) {
			$prefix .= $this->host . '::';	
		}
		if ($this->port) {
			$prefix .= $this->port . '::';	
		}
		$prefix .= $this->database . '::';
		if ($this->username) {
			$prefix .= $this->username . '::';	
		}
		return $prefix;	
	}
	
	
	/**
	 * Prepares the SQL by escaping values, spliting queries, cleaning escaped semicolons, fixing backslashed single quotes and translating
	 * 
	 * @param  string  $sql        The SQL to prepare
	 * @param  array   $values     Literal values to escape into the SQL
	 * @param  boolean $translate  If the SQL should be translated
	 * @return array  The split out SQL queries, queries that have been translated will have a string key of the original SQL, non-translated SQL will have a numeric key
	 */
	private function prepareSQL($sql, $values, $translate)
	{
		$this->connectToDatabase();
		
		// Ensure an SQL statement was passed
		if (empty($sql)) {
			throw new fProgrammerException('No SQL statement passed');
		}
		
		if ($values) {
			$sql = call_user_func_array(
				$this->escape,
				array_merge(array($sql), $values)
			);	
		}
		
		// Fix \' in MySQL and PostgreSQL
		if(($this->type == 'mysql' || $this->type == 'postgresql') && strpos($sql, '\\') !== FALSE) {
			$sql = preg_replace("#(?<!\\\\)((\\\\{2})*)\\\\'#", "\\1''", $sql);	
		}
		
		$strings = array(array());
		$queries = array('');
		$number  = 0;
		
		// Separate the SQL from quoted values
		preg_match_all("#(?:'([^']*(?:'')*)*?')|(?:[^']+)#", $sql, $matches);
		
		foreach ($matches[0] as $match) {
			if ($match[0] == "'") {
				$queries[$number]  .= ':string_' . sizeof($strings[$number]);
				$strings[$number][] = $match;	
			} else {
				$split_queries = preg_split('#(?<!\\\\);#', $match);
				
				$queries[$number] .= $split_queries[0];
				
				for ($i=1; $i < sizeof($split_queries); $i++) {
					$queries[$number] = trim($queries[$number]);
					$number++;
					$strings[$number] = array();
					$queries[$number] = $split_queries[$i];
				}	
			} 		
		}
		if (!trim($queries[$number])) {
			unset($queries[$number]);
			unset($strings[$number]);	
		} else {
			$queries[$number] = trim($queries[$number]);	
		}
		
		// Translate the SQL queries, this takes care of unescaping and reinserting strings
		if ($translate) {
			$output = $this->getSQLTranslation()->translate($queries, $strings);
			
		// For untranslated queries we need to unescape and reinsert strings
		} else {
			$output = array();
			foreach ($queries as $number => $query) {
				// Unescape literal semicolons in the queries
				$query = preg_replace('#(?<!\\\\)\\\\;#', ';', $query);
				// Put the strings back into the SQL
				foreach ($strings[$number] as $index => $string) {
					$string = strtr($string, array('\\' => '\\\\', '$' => '\\$'));
					$query  = preg_replace('#:string_' . $index . '\b#', $string, $query, 1);
				}
				$output[] = $query;
			}	
		}
		
		return $output;
	}
	
	
	/**
	 * Executes one or more SQL queries
	 * 
	 * @param  string $sql    One or more SQL statements
	 * @param  mixed  $value  The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
	 * @param  mixed  ...
	 * @return fResult|array  The fResult object(s) for the query
	 */
	public function query($sql)
	{
		$args    = func_get_args();
		$queries = $this->prepareSQL(
			$sql,
			array_slice($args, 1),
			FALSE
		);
		
		$output = array();
		foreach ($queries as $query) {
			$output[] = $this->runQuery($query, 'fResult');	
		}
		
		return sizeof($output) == 1 ? $output[0] : $output;
	}
	
	
	/**
	 * Runs a single query and times it, removes any old unbuffered queries before starting
	 * 
	 * @param  string $sql          The SQL statement to execute
	 * @param  string $result_type  The type of result object to return, fResult or fUnbufferedResult
	 * @return fResult|fUnbufferedResult  The result for the query
	 */
	private function runQuery($sql, $result_type)
	{
		if ($this->unbuffered_result) {
			$this->unbuffered_result->__destruct();
			$this->unbuffered_result = NULL;
		}
		
		$start_time = microtime(TRUE);	
			
		if (!$result = $this->handleTransactionQueries($sql, $result_type)) {
			$result = new $result_type($this, $this->type == 'mssql' ? $this->schema_info['character_set'] : NULL);
			$result->setSQL($sql);
			
			if ($result_type == 'fResult') {
				$this->executeQuery($result);
			} else {
				$this->executeUnbufferedQuery($result);	
			}
		}
		
		// Write some debugging info
		$query_time = microtime(TRUE) - $start_time;
		$this->query_time += $query_time;
		fCore::debug(
			self::compose(
				'Query time was %1$s seconds for:%2$s',
				$query_time,
				"\n" . $result->getSQL()
			),
			$this->debug
		);
		
		if ($this->slow_query_threshold && $query_time > $this->slow_query_threshold) {
			trigger_error(
				self::compose(
					'The following query took %1$s milliseconds, which is above the slow query threshold of %2$s:%3$s',
					$query_time,
					$this->slow_query_threshold,
					"\n" . $result->getSQL()
				),
				E_USER_WARNING
			);
		}
		
		return $result;
	}
	
	
	/**
	 * Turns an array possibly containing objects into an array of all strings
	 * 
	 * @param  array $values  The array of values to scalarize
	 * @return array  The scalarized values
	 */
	private function scalarize($values)
	{
		$new_values = array();
		foreach ($values as $value) {
			if (is_object($value) && is_callable(array($value, '__toString'))) {
				$value = $value->__toString();
			} elseif (is_object($value)) {
				$value = (string) $value;	
			} elseif (is_array($value)) {
				$value = $this->scalarize($value);	
			}
			$new_values[] = $value;
		}
		return $new_values;	
	}
	
	
	/**
	 * Sets the number of rows affected by the query
	 * 
	 * @param  fResult $result    The result object for the query
	 * @param  mixed   $resource  Only applicable for `pdo`, `oci8`, `odbc` and `sqlsrv` extentions, this is either the `PDOStatement` object or the `oci8`, `odbc` or `sqlsrv` resource
	 * @return void
	 */
	private function setAffectedRows($result, $resource=NULL)
	{
		if ($this->extension == 'mssql') {
			$affected_rows_result = mssql_query('SELECT @@ROWCOUNT AS rows', $this->connection);
			$result->setAffectedRows((int) mssql_result($affected_rows_result, 0, 'rows'));
		} elseif ($this->extension == 'mysql') {
			$result->setAffectedRows(mysql_affected_rows($this->connection));
		} elseif ($this->extension == 'mysqli') {
			$result->setAffectedRows(mysqli_affected_rows($this->connection));
		} elseif ($this->extension == 'oci8') {
			$result->setAffectedRows(oci_num_rows($resource));
		} elseif ($this->extension == 'odbc') {
			$result->setAffectedRows(odbc_num_rows($resource));
		} elseif ($this->extension == 'pgsql') {
			$result->setAffectedRows(pg_affected_rows($result->getResult()));
		} elseif ($this->extension == 'sqlite') {
			$result->setAffectedRows(sqlite_changes($this->connection));
		} elseif ($this->extension == 'sqlsrv') {
			$result->setAffectedRows(sqlsrv_rows_affected($resource));
		} elseif ($this->extension == 'pdo') {
			// This fixes the fact that rowCount is not reset for non INSERT/UPDATE/DELETE statements
			try {
				if (!$resource || !$resource->fetch()) {
					throw new PDOException();
				}
				$result->setAffectedRows(0);
			} catch (PDOException $e) {
				// The SQLite PDO driver seems to return 1 when no rows are returned from a SELECT statement
				if ($this->type == 'sqlite' && $this->extension == 'pdo' && preg_match('#^\s*SELECT#i', $result->getSQL())) {
					$result->setAffectedRows(0);	
				} elseif (!$resource) {
					$result->setAffectedRows(0);
				} else {
					$result->setAffectedRows($resource->rowCount());
				}
			}
		}
	}
	
	
	/**
	 * Sets the number of rows returned by the query
	 * 
	 * @param  fResult $result  The result object for the query
	 * @return void
	 */
	private function setReturnedRows($result)
	{
		if (is_resource($result->getResult()) || is_object($result->getResult())) {
			if ($this->extension == 'mssql') {
				$result->setReturnedRows(mssql_num_rows($result->getResult()));
			} elseif ($this->extension == 'mysql') {
				$result->setReturnedRows(mysql_num_rows($result->getResult()));
			} elseif ($this->extension == 'mysqli') {
				$result->setReturnedRows(mysqli_num_rows($result->getResult()));
			} elseif ($this->extension == 'pgsql') {
				$result->setReturnedRows(pg_num_rows($result->getResult()));
			} elseif ($this->extension == 'sqlite') {
				$result->setReturnedRows(sqlite_num_rows($result->getResult()));
			}
		} elseif (is_array($result->getResult())) {
			$result->setReturnedRows(sizeof($result->getResult()));
		}
	}
	
	
	/**
	 * Translates the SQL statement using fSQLTranslation and executes it
	 * 
	 * @param  string $sql    One or more SQL statements
	 * @param  mixed  $value  The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
	 * @param  mixed  ...
	 * @return fResult|array  The fResult object(s) for the query
	 */
	public function translatedQuery($sql)
	{
		$args    = func_get_args();
		$queries = $this->prepareSQL(
			$sql,
			array_slice($args, 1),
			TRUE
		);
		
		$output = array();
		foreach ($queries as $original_query => $query) {
			$result = $this->runQuery($query, 'fResult');
			if (!is_numeric($original_query)) {
				$result->setUntranslatedSQL($original_query);	
			}
			$output[] = $result;
		}
		
		return sizeof($output) == 1 ? $output[0] : $output;
	}
	
	
	/**
	 * Executes a single SQL statement in unbuffered mode. This is optimal for
	 * large results sets since it does not load the whole result set into
	 * memory first. The gotcha is that only one unbuffered result can exist at
	 * one time. If another unbuffered query is executed, the old result will
	 * be deleted.
	 * 
	 * @param  string $sql    A single SQL statement
	 * @param  mixed  $value  The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
	 * @param  mixed  ...
	 * @return fUnbufferedResult  The result object for the unbuffered query
	 */
	public function unbufferedQuery($sql)
	{
		$args    = func_get_args();
		$queries = $this->prepareSQL(
			$sql,
			array_slice($args, 1),
			FALSE
		);
		
		if (sizeof($queries) > 1) {
			throw new fProgrammerException(
				'Only a single unbuffered query can be run at a time, however %d were passed',
				sizeof($queries)	
			);
		}
		
		$result = $this->runQuery($queries[0], 'fUnbufferedResult');
		
		$this->unbuffered_result = $result;
		
		return $result;
	}
	
	
	/**
	 * Translates the SQL statement using fSQLTranslation and then executes it
	 * in unbuffered mode. This is optimal for large results sets since it does
	 * not load the whole result set into memory first. The gotcha is that only
	 * one unbuffered result can exist at one time. If another unbuffered query
	 * is executed, the old result will be deleted.
	 * 
	 * @param  string $sql    A single SQL statement
	 * @param  mixed  $value  The optional value(s) to place into any placeholders in the SQL - see ::escape() for details
	 * @param  mixed  ...
	 * @return fUnbufferedResult  The result object for the unbuffered query
	 */
	public function unbufferedTranslatedQuery($sql)
	{
		$args    = func_get_args();
		$queries = $this->prepareSQL(
			$sql,
			array_slice($args, 1),
			TRUE
		);
		
		if (sizeof($queries) > 1) {
			throw new fProgrammerException(
				'Only a single unbuffered query can be run at a time, however %d were passed',
				sizeof($queries)	
			);
		}
		
		$query_keys     = array_keys($queries);
		$original_query = $query_keys[0];
		
		$result = $this->runQuery($queries[$original_query], 'fUnbufferedResult');
		$result->setUntranslatedSQL($original_query);
		
		$this->unbuffered_result = $result;
		
		return $result;
	}
	
	
	/**
	 * Unescapes a value coming out of a database based on its data type
	 * 
	 * The valid data types are:
	 * 
	 *  - `'blob'` (or `'%l'`)
	 *  - `'boolean'` (or `'%b'`)
	 *  - `'date'` (or `'%d'`)
	 *  - `'float'` (or `'%f'`)
	 *  - `'integer'` (or `'%i'`)
	 *  - `'string'` (also `'%s'`, `'varchar'`, `'char'` or `'text'`)
	 *  - `'time'` (or `'%t'`)
	 *  - `'timestamp'` (or `'%p'`)
	 * 
	 * @param  string $data_type  The data type being unescaped - see method description for valid values
	 * @param  mixed  $value      The value or array of values to unescape
	 * @return mixed  The unescaped value
	 */
	public function unescape($data_type, $value)
	{
		if ($value === NULL) {
			return $value;	
		}
		
		$callback = NULL;
		
		switch ($data_type) {
			// Testing showed that strings tend to be most common,
			// and moving this to the top of the switch statement
			// improved performance on read-heavy pages
			case 'string':
			case 'varchar':
			case 'char':
			case 'text':
			case '%s':
				return $value;
			
			case 'boolean':
			case '%b':
				$callback = $this->unescapeBoolean;
				break;
				
			case 'date':
			case '%d':
				$callback = $this->unescapeDate;
				break;
				
			case 'float':
			case '%f':
				return $value;
				
			case 'integer':
			case '%i':
				return $value;
			
			case 'time':
			case '%t':
				$callback = $this->unescapeTime;
				break;
				
			case 'timestamp':
			case '%p':
				$callback = $this->unescapeTimestamp;
				break;
			
			case 'blob':
			case '%l':
				$callback = $this->unescapeBlob;
				break;
		}
		
		if ($callback) {
			if (is_array($value)) {
				return array_map($callback, $value);	
			}
			return call_user_func($callback, $value);
		}	
		
		throw new fProgrammerException(
			'Unknown data type, %1$s, specified. Must be one of: %2$s.',
			$data_type,
			'blob, %l, boolean, %b, date, %d, float, %f, integer, %i, string, %s, time, %t, timestamp, %p'
		);	
	}
	
	
	/**
	 * Unescapes a blob coming out of the database
	 * 
	 * @param  string $value  The value to unescape
	 * @return binary  The binary data
	 */
	private function unescapeBlob($value)
	{
		$this->connectToDatabase();
		
		if ($this->extension == 'pgsql') {
			return pg_unescape_bytea($value);
		} elseif ($this->extension == 'pdo' && is_resource($value)) {
			return stream_get_contents($value);
		} elseif ($this->type == 'mssql' && (substr($this->database, 0, 4) == 'dsn:')) {
			return pack('H*', $value);
		} elseif ($this->extension == 'sqlite') {
			return pack('H*', $value);
		} else {
			return $value;
		}
	}
	
	
	/**
	 * Unescapes a boolean coming out of the database
	 * 
	 * @param  string $value  The value to unescape
	 * @return boolean  The boolean
	 */
	private function unescapeBoolean($value)
	{
		return ($value === 'f' || !$value) ? FALSE : TRUE;
	}
	
	
	/**
	 * Unescapes a date coming out of the database
	 * 
	 * @param  string $value  The value to unescape
	 * @return string  The date in YYYY-MM-DD format
	 */
	private function unescapeDate($value)
	{
		if ($this->extension == 'sqlsrv' && $value instanceof DateTime) {
			return $value->format('Y-m-d');
		} elseif ($this->type == 'mssql') {
			$value = preg_replace('#:\d{3}#', '', $value);
		}
		return date('Y-m-d', strtotime($value));
	}
	
	
	/**
	 * Unescapes a time coming out of the database
	 * 
	 * @param  string $value  The value to unescape
	 * @return string  The time in `HH:MM:SS` format
	 */
	private function unescapeTime($value)
	{
		if ($this->extension == 'sqlsrv' && $value instanceof DateTime) {
			return $value->format('H:i:s');
		} elseif ($this->type == 'mssql') {
			$value = preg_replace('#:\d{3}#', '', $value);
		}
		return date('H:i:s', strtotime($value));
	}
	
	
	/**
	 * Unescapes a timestamp coming out of the database
	 * 
	 * @param  string $value  The value to unescape
	 * @return string  The timestamp in `YYYY-MM-DD HH:MM:SS` format
	 */
	private function unescapeTimestamp($value)
	{
		if ($this->extension == 'sqlsrv' && $value instanceof DateTime) {
			return $value->format('Y-m-d H:i:s');
		} elseif ($this->type == 'mssql') {
			$value = preg_replace('#:\d{3}#', '', $value);
		}
		return date('Y-m-d H:i:s', strtotime($value));
	}
}



/**
 * Copyright (c) 2007-2009 Will Bond <will@flourishlib.com>
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */